package com.bikeemotion.quartz.jobstore.hazelcast; import static org.quartz.Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY; import static org.testng.Assert.assertEquals; import java.util.Date; import java.util.List; import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.JobPersistenceException; import org.quartz.spi.JobStore; import org.quartz.spi.OperableTrigger; import org.testng.annotations.Test; import com.bikeemotion.quartz.AbstractTest; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import org.testng.annotations.AfterMethod; import java.util.concurrent.ThreadLocalRandom; /** * Tests running {@link HazelcastJobStore} on two different nodes. If one node crashes during * {@link JobStore#acquireNextTriggers(long, int, long)}, the second node should still be able * to get the triggers. * <p> * This test is unstable in a way that it might succeed when it should in fact fail. However, it * will never fail when it should succeed. * </p> * <p> * The reason it is unstable is because we need to time our termination of Hazelcast so it is done * right in the middle of {@link JobStore#acquireNextTriggers(long, int, long)}, and that doesn't * always happen. * </p> * * @author Anton Johansson */ public class HazelcastJobStoreUngracefulShutdownTest extends AbstractTest { @AfterMethod public void tearDown() { Hazelcast.shutdownAll(); } @Test public void testOneOfTwoInstancesCrashing() throws Exception { // Build node 1 HazelcastInstance hazelcast1 = createHazelcastInstance("testOneOfTwoInstancesCrashing"); HazelcastJobStore.setHazelcastClient(hazelcast1); HazelcastJobStore jobstore1 = createJobStore("jobstore1"); jobstore1.setTriggerReleaseThreshold(450); jobstore1.setShutdownHazelcastOnShutdown(false); jobstore1.initialize(null, new SampleSignaler()); // Build node 2 HazelcastInstance hazelcast2 = createHazelcastInstance("testOneOfTwoInstancesCrashing"); HazelcastJobStore.setHazelcastClient(hazelcast2); HazelcastJobStore jobstore2 = createJobStore("jobstore2"); jobstore2.setShutdownHazelcastOnShutdown(false); jobstore2.setTriggerReleaseThreshold(450); jobstore2.initialize(null, new SampleSignaler()); // Add a job and its trigger to the scheduler JobDetail job = JobBuilder.newJob(TestSlowJob.class).withIdentity("job1", "jobGroup1").build(); OperableTrigger trigger = buildAndComputeTrigger("trigger1", "triggerGroup1", job, new Date().getTime()); trigger.setMisfireInstruction(MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY); jobstore1.storeJobAndTrigger(job, (OperableTrigger) trigger); long firstFireTime = new Date(trigger.getNextFireTime().getTime()).getTime(); // Create a thread for acquiring next triggers on node 1 Thread acquireThread = new Thread(() -> { try { List<OperableTrigger> triggers1 = jobstore1.acquireNextTriggers(firstFireTime + 150, 1, 0L); triggers1.forEach(jobstore1::releaseAcquiredTrigger); } catch (JobPersistenceException e) { throw new RuntimeException(e); } }, "acquireThread"); // Create a thread for terminating Hazelcast on node 1 Thread terminateThread = new Thread(hazelcast1.getLifecycleService()::terminate, "terminateThread"); // Start acquiring next triggers and right after start terminating Hazelcast acquireThread.start(); long waitTime = ThreadLocalRandom.current().nextInt(1,51); Thread.sleep(waitTime); terminateThread.start(); // Wait a bit Thread.sleep(500); // Acquire next triggers on node 2, we should get our trigger here! List<OperableTrigger> triggers2 = jobstore2.acquireNextTriggers(firstFireTime + 150 + 6000, 10, 0L); System.err.println("-------------------------> VAL " + triggers2.size()); assertEquals(triggers2.size(), 1, "Should find 1 trigger on node 2 after node 1 crashed when failing after "+waitTime+"ms"); } }